package cn.seaboot.admin.security.auth;

import cn.seaboot.commons.core.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.ConsensusBased;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;

/**
 * 投票决策
 *
 * 权限类型配置，有两份配置：url 配置一份，用户配置一份；
 * 用户和 url 匹配上了，就说明有权限访问。
 * <p>
 * 定义了一套全新的鉴权规则：
 * 白名单/匿名访问名单不参与投票，直接放开全部权限；
 * 目前可选：一票通过/全票通过两种方式。
 * <p>
 * 默认实现，代码参考:
 * - {@link AffirmativeBased}   需要所有的投票赞成才能通过；
 * - {@link ConsensusBased}     需要大多数投票赞成即可通过；
 * - {@link UnanimousBased}     只需有一个投票赞成即可通过；
 *
 * @author Mr.css
 * @version 2022-01-06 19:32
 */
@Component
public class AntAuthorityAccessDecision implements AccessDecisionManager {
    private final Logger logger = LoggerFactory.getLogger(AntAuthorityAccessDecision.class);

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Resource
    private SecurityAuthorityProvider securityAuthorityProvider;

    /**
     * 表示当前类可以解析
     *
     * @param clazz -
     * @return -
     */
    @Override
    public boolean supports(Class<?> clazz) {
        if (FilterInvocation.class == clazz) {
            return true;
        } else {
            logger.debug("unknown login filter：{}", clazz);
            throw new AccessDeniedException("Unknown login filter：" + clazz);
        }
    }

    /**
     * 只校验用户为 authenticated 的权限
     * <p>
     * 因为没有我们没有做其它配置，因此只会出现 authenticated 一种情况
     *
     * @param attribute -
     * @return -
     */
    @Override
    public boolean supports(ConfigAttribute attribute) {
        if (AuthorityType.AUTHENTICATED.equals(attribute.toString())) {
            return true;
        } else {
            logger.debug("unknown login config-attribute：{}", attribute);
            throw new AccessDeniedException("Unknown login config-attribute：" + attribute);
        }
    }


    /**
     * 与角色对比，查看是否满足权限
     *
     * 采用了默认拒绝的策略，根据用户的授权信息，逐步放开权限。
     *
     * @param authentication   用户凭证
     * @param object           拦截调用
     * @param configAttributes 因为没有设置前置查询，理论上不应该存在authenticated之外的值
     * @throws AccessDeniedException -
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        logger.debug("[security authority decision] decide start!");
        logger.debug("[security authority decision] authentication type : {}", authentication.getClass());

        FilterInvocation filterInvocation = (FilterInvocation) object;
        HttpServletRequest request = filterInvocation.getRequest();
        String url = request.getServletPath();
        String requestMethod = filterInvocation.getHttpRequest().getMethod();

        // 登录即可访问，不区用户
        if (isAuthenticated(url, requestMethod)) {
            logger.debug("[security authority decision] authenticated!");
            return;
        }

        // 验证匿名访问名单
        if(authentication instanceof AnonymousAuthenticationToken){
            if (isAnonymous(url, requestMethod)) {
                // 匿名账号投票决策通过
                logger.debug("[security authority decision] anonymous user : {}", authentication.getPrincipal());
                return;
            } else {
                // 默认拒绝
                logger.error("[security authority decision] access denied!");
                throw new AccessDeniedException("access denied!");
            }
        }

        // 投票决策
        Collection<AntAuthority> antAuthorities = securityAuthorityProvider.queryHasAuthorityAnt();
        logger.debug("[security authority decision] ant: {}", antAuthorities);
        if (CommonUtils.isNotEmpty(antAuthorities)) {
            Collection<? extends GrantedAuthority> grantedAuthorities = authentication.getAuthorities();
            if(CommonUtils.isNotEmpty(grantedAuthorities)){
                logger.debug("[security authority decision] granted: {}", grantedAuthorities);
                // 使用一票通过策略：只要满足一个条件，即可进行页面访问
                if (voteAffirmative(url, requestMethod, antAuthorities, grantedAuthorities)) {
                    logger.debug("[security authority decision] decide complete!");
                    return;
                }
            }
        }

        // 默认拒绝
        logger.error("[security authority decision] access denied!");
        throw new AccessDeniedException("access denied!");
    }

    /**
     * 是否命中一个权限配置
     *
     * @param url           客户端请求路径
     * @param requestMethod 客户端请求方式
     * @param authority     路径权限配置
     * @return 是否命中
     */
    private boolean match(String url, String requestMethod, AntAuthority authority) {
        // 路径 & 请求方式要匹配
        if (antPathMatcher.match(authority.getAnt(), url)) {
            return authority.getMethod() == null || "ALL".equals(authority.getMethod()) || authority.getMethod().equals(requestMethod);
        }
        return false;
    }

    /**
     * 是否处于白名单
     *
     * @param url url
     * @return 是否满足
     */
    private boolean isAuthenticated(String url, String requestMethod) {
        Collection<AntAuthority> authorities = securityAuthorityProvider.queryAuthenticatedAnt();
        for (AntAuthority authority : authorities) {
            if (this.match(url, requestMethod, authority)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 是否允许匿名访问
     *
     * @param url url
     * @return 是否满足
     */
    private boolean isAnonymous(String url, String requestMethod) {
        Collection<AntAuthority> authorities = securityAuthorityProvider.queryAnonymousAnt();
        for (AntAuthority authority : authorities) {
            if (this.match(url, requestMethod, authority)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 权限匹配
     * <p>
     * hasRole()规则，满足权限中的任何一个
     *
     * @param authorities        Url所需的权限列表
     * @param grantedAuthorities 用户所拥有的权限(包含角色/权限)
     * @return 是否命中
     */
    private boolean hasAuthority(String[] authorities, Collection<? extends GrantedAuthority> grantedAuthorities) {
        for (int i = 1; i < authorities.length; i++) {
            String authority = authorities[i];
            for (GrantedAuthority grantedAuthority : grantedAuthorities) {
                if (authority.equals(grantedAuthority.getAuthority())) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * 权限匹配
     * <p>
     * hasAllRole()规则，满足规定的所有权限
     *
     * @param authorities        Url所需的权限列表
     * @param grantedAuthorities 用户所拥有的权限(包含角色/权限)
     * @return 是否命中
     */
    private boolean hasAllAuthority(String[] authorities, Collection<? extends GrantedAuthority> grantedAuthorities) {
        boolean hit;
        for (int i = 1; i < authorities.length; i++) {
            String authority = authorities[i];
            hit = false;
            for (GrantedAuthority grantedAuthority : grantedAuthorities) {
                hit = authority.equals(grantedAuthority.getAuthority());
                if (hit) {
                    break;
                }
            }
            if (!hit) {
                return false;
            }
        }
        return true;
    }

    /**
     * 是否满足权限配置
     * <p>
     * Security 没有权限和角色的区别，代码是同一套，
     *
     * @param chain              权限链对象
     * @param grantedAuthorities 分配给角色的权限
     * @return 是否命中
     */
    private boolean hitAuthority(AntAuthority chain, Collection<? extends GrantedAuthority> grantedAuthorities) {
        String[] authorities = chain.getAuthority();
        String type = authorities[0];
        if (type.startsWith("hasAll")) {
            return hasAllAuthority(authorities, grantedAuthorities);
        } else {
            return hasAuthority(authorities, grantedAuthorities);
        }
    }

    /**
     * Unanimous 投票规则：
     * <p>
     * 按照权限链顺序执行，必须满足全部权限配置才能进行访问。
     *
     * @param url                请求路径
     * @param requestMethod      请求方式
     * @param securityChainList  权限链
     * @param grantedAuthorities 用户拥有的权限
     */
    private boolean voteUnanimous(String url, String requestMethod, Collection<AntAuthority> securityChainList, Collection<? extends GrantedAuthority> grantedAuthorities) {
        for (AntAuthority authority : securityChainList) {
            if (this.match(url, requestMethod, authority)) {
                // 一个不命中，则直接鉴权失败
                if (!hitAuthority(authority, grantedAuthorities)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Affirmative 投票规则：
     * <p>
     * 按照权限链顺序执行，只需要满足一个权限配置即可访问。
     *
     * @param url                请求路径
     * @param requestMethod      请求方式
     * @param antAuthorities     权限链
     * @param grantedAuthorities 用户拥有的权限
     */
    private boolean voteAffirmative(String url, String requestMethod, Collection<AntAuthority> antAuthorities, Collection<? extends GrantedAuthority> grantedAuthorities) {
        for (AntAuthority authority : antAuthorities) {
            if (this.match(url, requestMethod, authority)) {
                // 命中一个，直接校验
                if (hitAuthority(authority, grantedAuthorities)) {
                    return true;
                }
            }
        }
        return false;
    }
}
